package cn.itcast.hotel.service.impl;

import cn.itcast.hotel.dto.PageResult;
import cn.itcast.hotel.dto.RequestParams;
import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {


    @Autowired
    private RestHighLevelClient client;


    /**
     * 自动补全
     * @param key
     * @return
     */
    @Override
    public List<String> suggestions(String key) {

        //参数校验
        if (StringUtils.isBlank(key)) {
            return new ArrayList<>();
        }
        SearchRequest request = new SearchRequest("hotel");
        request.source()
                .suggest(
                        new SuggestBuilder()
                                .addSuggestion("nameSugg",
                                        SuggestBuilders
                                                .completionSuggestion("suggestion")
                                                .prefix(key)
                                                .skipDuplicates(true)
                                                .size(10)
                                )
                );

        SearchResponse response = null;
        try {
            response = client.search(request, RequestOptions.DEFAULT);
            Suggest suggest = response.getSuggest();
            CompletionSuggestion nameSugg = suggest.getSuggestion("nameSugg");
            if (nameSugg != null) {
                List<CompletionSuggestion.Entry.Option> options = nameSugg.getOptions();
                List<String> result = new ArrayList<>();
                for (CompletionSuggestion.Entry.Option option : options) {
                    result.add(option.getText().string());
                }
                return result;
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ArrayList<>();
    }

    /**
     * 动态筛选条件(聚合查询)
     * @param params 参数(和搜索条件保持一致)
     * @return
     */
    @Override
    public Map<String, List<String>> filters(RequestParams params) {
        //1 构建条件
        SearchRequest request = new SearchRequest("hotel");
        buildBasicQuery(params, request);

        //2 聚合查询
        //忽略原始数据
        request.source().size(0);
        //聚合
        request.source().aggregation(
                AggregationBuilders
                        .terms("cityAgg")
                        .order(BucketOrder.count(true))
                        .field("city")
                        .size(20)
        );
        request.source().aggregation(
                AggregationBuilders
                        .terms("starAgg")
                        .order(BucketOrder.count(true))
                        .field("starName")
                        .size(20)
        );

        request.source().aggregation(
                AggregationBuilders
                        .terms("brandAgg")
                        .order(BucketOrder.count(true))
                        .field("brand")
                        .size(20)
        );

        SearchResponse searchResponse = null;
        try {
            searchResponse = client.search(request, RequestOptions.DEFAULT);
            Aggregations aggregations = searchResponse.getAggregations();
            //解析城市
            Terms cityAgg = aggregations.get("cityAgg");
            List<String> citys = new ArrayList<>();
            List<? extends Terms.Bucket> cityBuckets = cityAgg.getBuckets();
            for (Terms.Bucket bucket : cityBuckets) {
                String key = bucket.getKeyAsString();
                citys.add(key);
            }
            //解析星级
            Terms starAgg = aggregations.get("starAgg");
            List<String> stars = new ArrayList<>();
            List<? extends Terms.Bucket> starBuckets = starAgg.getBuckets();
            for (Terms.Bucket bucket : starBuckets) {
                String key = bucket.getKeyAsString();
                stars.add(key);
            }

            //解析品牌
            Terms brandAgg = aggregations.get("brandAgg");
            List<String> brands = new ArrayList<>();
            List<? extends Terms.Bucket> brandBuckets = brandAgg.getBuckets();
            for (Terms.Bucket bucket : brandBuckets) {
                String key = bucket.getKeyAsString();
                brands.add(key);
            }

            Map<String, List<String>> result = new HashMap<>();
            result.put("city", citys);
            result.put("starName", stars);
            result.put("brand", brands);

            return result;

        } catch (IOException e) {
            e.printStackTrace();
            log.error("filters error , {}", e.getMessage());
        }
        return new HashMap<>();
    }

    /**
     * 构建查询条件
     * @param params
     * @param request
     */
    private void buildBasicQuery(RequestParams params, SearchRequest request) {
        //1 构建查询条件
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //1.1 按照关键词查询
        String key = params.getKey();
        if (StringUtils.isNotBlank(key)) {
            boolQuery.must(QueryBuilders.matchQuery("all", key));
        }else {
            boolQuery.must(QueryBuilders.matchAllQuery());
        }

        //1.2 条件过滤
        String city = params.getCity();
        if (StringUtils.isNotBlank(city)) {
            boolQuery.filter(QueryBuilders.termQuery("city", city));
        }
        String starName = params.getStarName();
        if (StringUtils.isNotBlank(starName)) {
            boolQuery.filter(QueryBuilders.termQuery("starName", starName));
        }
        String brand = params.getBrand();
        if (StringUtils.isNotBlank(brand)) {
            boolQuery.filter(QueryBuilders.termQuery("brand", brand));
        }
        Integer minPrice = params.getMinPrice();
        Integer maxPrice = params.getMaxPrice();
        if (minPrice != null && maxPrice != null) {
            boolQuery.filter(QueryBuilders.rangeQuery("price").gt(minPrice).lte(maxPrice));
        }

        //*** 广告算分
        String script = "return doc['adScore'].value"; // 按照广告分数排名
        FunctionScoreQueryBuilder functionScoreQueryBuilder =
                QueryBuilders.functionScoreQuery(boolQuery,
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                        QueryBuilders.termQuery("isAD", true),
                                        //ScoreFunctionBuilders.weightFactorFunction(10)
                                        ScoreFunctionBuilders.scriptFunction(new Script(script))
                                )
                        }
                );

        request.source()
                .query(functionScoreQueryBuilder)
                //分页
                .from( (params.getPage() - 1) * params.getSize() )
                .size(params.getSize())
                .trackTotalHits(true);
    }
    /**
     * 条件搜索
     * @param params 参数
     * @return
     */
    @Override
    public PageResult search(RequestParams params) {
        SearchRequest request = new SearchRequest("hotel");

        // 构建查询条件
        buildBasicQuery(params, request);

        //设置高亮 三要素
        request.source().highlighter(
                SearchSourceBuilder.highlight()
                        .field("name")
                        .preTags("<em>")
                        .postTags("</em>")
                        .requireFieldMatch(false)
        );

        String sortBy = params.getSortBy();
        if (StringUtils.isNotBlank(sortBy) && !sortBy.contains("default")) {
            String[] split = sortBy.split("-");
            request.source().sort(split[0], SortOrder.fromString(split[1]));
        }

        //我的附近
        String location = params.getLocation();  //当前位置
        if (StringUtils.isNotBlank(location)){
            request.source().sort(SortBuilders
                    .geoDistanceSort("location", new GeoPoint(params.getLocation()))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS)
            );
        }


        //2 执行查询
        SearchResponse response = null;
        try {
            response = client.search(request, RequestOptions.DEFAULT);
            //3 解析结果
            SearchHits searchHits = response.getHits();
            long total = searchHits.getTotalHits().value;

            List<HotelDoc> hotelDocs = new ArrayList<>();
            SearchHit[] hits = searchHits.getHits();
            for (SearchHit hit : hits) {
                String json = hit.getSourceAsString();
                HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);

                //*** 获取高亮
                HighlightField highlightField = hit.getHighlightFields().get("name");
                if (highlightField != null) {
                    Text[] fragments = highlightField.getFragments();
                    if (fragments != null && fragments.length > 0) {
                        String nameResult = StringUtils.join(fragments);// 高亮片段
                        if (StringUtils.isNotBlank(nameResult)) {
                            hotelDoc.setName(nameResult);
                        }
                    }
                }
                //解析距离
                Object[] sortValues = hit.getSortValues();
                if (sortValues != null && sortValues.length > 0) {
                    hotelDoc.setDistance(sortValues[0]);
                }

                hotelDocs.add(hotelDoc);
            }
            return new PageResult(total, hotelDocs);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return new PageResult();

    }


    @Override
    public void deleteById(Long id) {
        try {
            // 1.准备Request
            DeleteRequest request = new DeleteRequest("hotel", id.toString());
            // 2.发送请求
            client.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void insertById(Long id) {
        try {
            // 0.根据id查询酒店数据
            Hotel hotel = getById(id);
            // 转换为文档类型
            HotelDoc hotelDoc = new HotelDoc(hotel);
            // 1.准备Request对象
            IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
            // 2.准备Json文档
            request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
            // 3.发送请求
            client.index(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
